Maîtrisez les combinateurs de Promesses JavaScript (Promise.all, Promise.allSettled, Promise.race, Promise.any) pour une programmation asynchrone efficace et robuste dans les applications globales.
Combinateurs de Promesses JavaScript : Patrons Asynchrones Avancés pour les Applications Globales
La programmation asynchrone est une pierre angulaire du JavaScript moderne, en particulier lors de la création d'applications web qui interagissent avec des API, des bases de données ou effectuent des opérations chronophages. Les Promesses JavaScript offrent une abstraction puissante pour gérer les opérations asynchrones, mais leur maîtrise nécessite la compréhension de patrons avancés. Cet article explore les combinateurs de Promesses JavaScript – Promise.all, Promise.allSettled, Promise.race, et Promise.any – et comment ils peuvent être utilisés pour créer des flux de travail asynchrones efficaces et robustes, particulièrement dans le contexte d'applications globales avec des conditions de réseau et des sources de données variables.
Comprendre les Promesses : Un Bref Récapitulatif
Avant de plonger dans les combinateurs, revoyons rapidement les Promesses. Une Promesse (Promise) représente le résultat éventuel d'une opération asynchrone. Elle peut être dans l'un des trois états suivants :
- En attente (Pending): L'état initial, ni accomplie ni rejetée.
- Accomplie (Fulfilled): L'opération s'est terminée avec succès, avec une valeur résultante.
- Rejetée (Rejected): L'opération a échoué, avec une raison (généralement un objet Error).
Les Promesses offrent un moyen plus propre et plus gérable de traiter les opérations asynchrones par rapport aux callbacks traditionnels. Elles améliorent la lisibilité du code et simplifient la gestion des erreurs. Surtout, elles forment la base des combinateurs de Promesses que nous allons explorer.
Les Combinateurs de Promesses : Orchestrer les Opérations Asynchrones
Les combinateurs de Promesses sont des méthodes statiques sur l'objet Promise qui vous permettent de gérer et de coordonner plusieurs Promesses. Ils fournissent des outils puissants pour construire des flux de travail asynchrones complexes. Examinons chacun d'eux en détail.
Promise.all() : Exécuter les Promesses en Parallèle et Agréger les Résultats
Promise.all() prend un itérable (généralement un tableau) de Promesses en entrée et retourne une seule Promesse. Cette Promesse retournée est accomplie lorsque toutes les Promesses en entrée ont été accomplies. Si l'une des Promesses en entrée est rejetée, la Promesse retournée est immédiatement rejetée avec la raison de la première Promesse rejetée.
Cas d'utilisation : Lorsque vous devez récupérer des données de plusieurs API simultanément et traiter les résultats combinés, Promise.all() est idéal. Par exemple, imaginez la construction d'un tableau de bord qui affiche les informations météorologiques de différentes villes du monde. Les données de chaque ville pourraient être récupérées via un appel API distinct.
async function fetchWeatherData(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`); // Remplacer par un véritable point de terminaison d'API
if (!response.ok) {
throw new Error(`Échec de la récupération des données météo pour ${city}`);
}
return await response.json();
} catch (error) {
console.error(`Erreur lors de la récupération des données météo pour ${city}: ${error}`);
throw error; // Relancer l'erreur pour qu'elle soit capturée par Promise.all
}
}
async function displayWeatherData() {
const cities = ['Londres', 'Tokyo', 'New York', 'Sydney'];
try {
const weatherDataPromises = cities.map(city => fetchWeatherData(city));
const weatherData = await Promise.all(weatherDataPromises);
weatherData.forEach((data, index) => {
console.log(`Météo à ${cities[index]}:`, data);
// Mettre à jour l'interface utilisateur avec les données météo
});
} catch (error) {
console.error('Échec de la récupération des données météo pour toutes les villes :', error);
// Afficher un message d'erreur Ă l'utilisateur
}
}
displayWeatherData();
Considérations pour les Applications Globales :
- Latence Réseau : Les requêtes vers différentes API dans différentes zones géographiques peuvent connaître des latences variables.
Promise.all()ne garantit pas l'ordre dans lequel les Promesses sont accomplies, seulement qu'elles sont toutes accomplies (ou qu'une est rejetée) avant que la Promesse combinée ne se résolve. - Limitation de Débit des API : Si vous effectuez plusieurs requêtes vers la même API ou plusieurs API avec des limites de débit partagées, vous pourriez dépasser ces limites. Mettez en œuvre des stratégies comme la mise en file d'attente des requêtes ou l'utilisation d'un backoff exponentiel pour gérer la limitation de débit avec élégance.
- Gestion des Erreurs : Rappelez-vous que si n'importe quelle Promesse est rejetée, toute l'opération
Promise.all()échoue. Cela peut ne pas être souhaitable si vous voulez afficher des données partielles même si certaines requêtes échouent. Envisagez d'utiliserPromise.allSettled()dans de tels cas (expliqué ci-dessous).
Promise.allSettled() : Gérer le Succès et l'Échec Individuellement
Promise.allSettled() est similaire à Promise.all(), mais avec une différence cruciale : il attend que toutes les Promesses en entrée se résolvent, qu'elles soient accomplies ou rejetées. La Promesse retournée est toujours accomplie avec un tableau d'objets, chacun décrivant le résultat de la Promesse correspondante en entrée. Chaque objet a une propriété status (soit "fulfilled" soit "rejected") et une propriété value (si accomplie) ou reason (si rejetée).
Cas d'utilisation : Lorsque vous devez collecter les résultats de plusieurs opérations asynchrones, et qu'il est acceptable que certaines échouent sans provoquer l'échec de l'opération entière, Promise.allSettled() est le meilleur choix. Imaginez un système qui traite les paiements via plusieurs passerelles de paiement. Vous pourriez vouloir tenter tous les paiements et enregistrer ceux qui ont réussi et ceux qui ont échoué.
async function processPayment(paymentGateway, amount) {
try {
const response = await paymentGateway.process(amount); // Remplacer par une véritable intégration de passerelle de paiement
if (response.status === 'success') {
return { status: 'fulfilled', value: `Paiement traité avec succès via ${paymentGateway.name}` };
} else {
throw new Error(`Paiement échoué via ${paymentGateway.name}: ${response.message}`);
}
} catch (error) {
return { status: 'rejected', reason: `Paiement échoué via ${paymentGateway.name}: ${error.message}` };
}
}
async function processMultiplePayments(paymentGateways, amount) {
const paymentPromises = paymentGateways.map(gateway => processPayment(gateway, amount));
const results = await Promise.allSettled(paymentPromises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
// Analyser les résultats pour déterminer le succès/échec global
const successfulPayments = results.filter(result => result.status === 'fulfilled').length;
const failedPayments = results.filter(result => result.status === 'rejected').length;
console.log(`Paiements réussis : ${successfulPayments}`);
console.log(`Paiements échoués : ${failedPayments}`);
}
// Exemples de passerelles de paiement
const paymentGateways = [
{ name: 'PayPal', process: (amount) => Promise.resolve({ status: 'success', message: 'Paiement réussi' }) },
{ name: 'Stripe', process: (amount) => Promise.reject({ status: 'error', message: 'Fonds insuffisants' }) },
{ name: 'Worldpay', process: (amount) => Promise.resolve({ status: 'success', message: 'Paiement réussi' }) },
];
processMultiplePayments(paymentGateways, 100);
Considérations pour les Applications Globales :
- Robustesse :
Promise.allSettled()améliore la robustesse de vos applications en garantissant que toutes les opérations asynchrones sont tentées, même si certaines échouent. C'est particulièrement important dans les systèmes distribués où les pannes sont courantes. - Rapports Détaillés : Le tableau de résultats fournit des informations détaillées sur l'issue de chaque opération, vous permettant de journaliser les erreurs, de relancer les opérations échouées ou de fournir aux utilisateurs un retour spécifique.
- Succès Partiel : Vous pouvez facilement déterminer le taux de réussite global et prendre les mesures appropriées en fonction du nombre d'opérations réussies et échouées. Par exemple, vous pourriez proposer d'autres méthodes de paiement si la passerelle principale échoue.
Promise.race() : Choisir le Résultat le Plus Rapide
Promise.race() prend également un itérable de Promesses en entrée et retourne une seule Promesse. Cependant, contrairement à Promise.all() et Promise.allSettled(), Promise.race() se résout dès que l'une des Promesses en entrée se résout (qu'elle soit accomplie ou rejetée). La Promesse retournée est accomplie ou rejetée avec la valeur ou la raison de la première Promesse résolue.
Cas d'utilisation : Lorsque vous avez besoin de sélectionner la réponse la plus rapide parmi plusieurs sources, Promise.race() est un bon choix. Imaginez interroger plusieurs serveurs pour les mêmes données et utiliser la première réponse que vous recevez. Cela peut améliorer les performances et la réactivité, en particulier dans les situations où certains serveurs pourraient être temporairement indisponibles ou plus lents que d'autres.
async function fetchDataFromServer(serverURL) {
try {
const response = await fetch(serverURL, {signal: AbortSignal.timeout(5000)}); // Ajouter un timeout de 5 secondes
if (!response.ok) {
throw new Error(`Échec de la récupération des données depuis ${serverURL}`);
}
return await response.json();
} catch (error) {
console.error(`Erreur lors de la récupération des données depuis ${serverURL}: ${error}`);
throw error;
}
}
async function getFastestResponse() {
const serverURLs = [
'https://server1.example.com/data', // Remplacer par les vraies URL des serveurs
'https://server2.example.com/data',
'https://server3.example.com/data',
];
try {
const dataPromises = serverURLs.map(serverURL => fetchDataFromServer(serverURL));
const fastestData = await Promise.race(dataPromises);
console.log('Données les plus rapides reçues :', fastestData);
// Utiliser les données les plus rapides
} catch (error) {
console.error('Échec de la récupération des données depuis tous les serveurs :', error);
// Gérer l'erreur
}
}
getFastestResponse();
Considérations pour les Applications Globales :
- Délais d'attente (Timeouts) : Il est crucial de mettre en place des délais d'attente lors de l'utilisation de
Promise.race()pour éviter que la Promesse retournée n'attende indéfiniment si certaines des Promesses en entrée ne se résolvent jamais. L'exemple ci-dessus utilise `AbortSignal.timeout()` pour y parvenir. - Conditions Réseau : Le serveur le plus rapide peut varier en fonction de la localisation géographique de l'utilisateur et des conditions du réseau. Envisagez d'utiliser un Réseau de Diffusion de Contenu (CDN) pour distribuer votre contenu et améliorer les performances pour les utilisateurs du monde entier.
- Gestion des Erreurs : Si la Promesse qui 'gagne' la course est rejetée, alors l'ensemble de Promise.race est rejeté. Assurez-vous que chaque Promesse a une gestion des erreurs appropriée pour éviter les rejets inattendus. De plus, si la promesse "gagnante" est rejetée en raison d'un timeout (comme montré ci-dessus), les autres promesses continueront de s'exécuter en arrière-plan. Vous pourriez avoir besoin d'ajouter une logique pour annuler ces autres promesses en utilisant `AbortController` si elles ne sont plus nécessaires.
Promise.any() : Accepter la Première Réussite
Promise.any() est similaire à Promise.race(), mais avec un comportement légèrement différent. Il attend que la première Promesse en entrée soit accomplie. Si toutes les Promesses en entrée sont rejetées, Promise.any() est rejeté avec une AggregateError contenant un tableau des raisons de rejet.
Cas d'utilisation : Lorsque vous devez récupérer des données de plusieurs sources et que seul le premier résultat réussi vous importe, Promise.any() est un bon choix. C'est utile lorsque vous avez des sources de données redondantes ou des API alternatives qui fournissent les mêmes informations. Il privilégie le succès à la vitesse, car il attend la première réussite, même si certaines Promesses sont rejetées rapidement.
async function fetchDataFromSource(sourceURL) {
try {
const response = await fetch(sourceURL);
if (!response.ok) {
throw new Error(`Échec de la récupération des données depuis ${sourceURL}`);
}
return await response.json();
} catch (error) {
console.error(`Erreur lors de la récupération des données depuis ${sourceURL}: ${error}`);
throw error;
}
}
async function getFirstSuccessfulData() {
const dataSources = [
'https://source1.example.com/data', // Remplacer par les vraies URL des sources de données
'https://source2.example.com/data',
'https://source3.example.com/data',
];
try {
const dataPromises = dataSources.map(sourceURL => fetchDataFromSource(sourceURL));
const data = await Promise.any(dataPromises);
console.log('Premières données réussies reçues :', data);
// Utiliser les données réussies
} catch (error) {
if (error instanceof AggregateError) {
console.error('Échec de la récupération des données depuis toutes les sources :', error.errors);
// Gérer l'erreur
} else {
console.error('Une erreur inattendue est survenue :', error);
}
}
}
getFirstSuccessfulData();
Considérations pour les Applications Globales :
- Redondance :
Promise.any()est particulièrement utile lorsqu'on traite avec des sources de données redondantes qui fournissent des informations similaires. Si une source est indisponible ou lente, vous pouvez compter sur les autres pour fournir les données. - Gestion des Erreurs : Assurez-vous de gérer l'
AggregateErrorqui est levĂ©e lorsque toutes les Promesses en entrĂ©e sont rejetĂ©es. Cette erreur contient un tableau des raisons de rejet individuelles, vous permettant de dĂ©boguer et de diagnostiquer les problèmes. - Priorisation : L'ordre dans lequel vous fournissez les Promesses Ă
Promise.any()a son importance. Placez les sources de données les plus fiables ou les plus rapides en premier pour augmenter la probabilité d'un résultat réussi.
Choisir le Bon Combinateur : Un Résumé
Voici un résumé rapide pour vous aider à choisir le combinateur de Promesses approprié à vos besoins :
- Promise.all() : À utiliser lorsque vous avez besoin que toutes les Promesses soient accomplies avec succès, et que vous voulez un échec immédiat si l'une d'elles est rejetée.
- Promise.allSettled() : À utiliser lorsque vous voulez attendre que toutes les Promesses se résolvent, indépendamment du succès ou de l'échec, et que vous avez besoin d'informations détaillées sur chaque résultat.
- Promise.race() : À utiliser lorsque vous voulez choisir le résultat le plus rapide parmi plusieurs Promesses, et que seul le premier qui se résout vous importe.
- Promise.any() : À utiliser lorsque vous voulez accepter le premier résultat réussi parmi plusieurs Promesses, et que cela ne vous dérange pas si certaines Promesses sont rejetées.
Patrons Avancés et Bonnes Pratiques
Au-delà de l'utilisation de base des combinateurs de Promesses, il existe plusieurs patrons avancés et bonnes pratiques à garder à l'esprit :
Limiter la Concurrence
Lorsque vous traitez un grand nombre de Promesses, les exécuter toutes en parallèle pourrait surcharger votre système ou dépasser les limites de débit des API. Vous pouvez limiter la concurrence en utilisant des techniques comme :
- Le découpage en lots (Chunking) : Divisez les Promesses en lots plus petits et traitez chaque lot séquentiellement.
- L'utilisation d'un sémaphore : Implémentez un sémaphore pour contrôler le nombre d'opérations concurrentes.
Voici un exemple utilisant le découpage en lots :
async function processInChunks(promises, chunkSize) {
const results = [];
for (let i = 0; i < promises.length; i += chunkSize) {
const chunk = promises.slice(i, i + chunkSize);
const chunkResults = await Promise.all(chunk);
results.push(...chunkResults);
}
return results;
}
// Exemple d'utilisation
const myPromises = [...Array(100)].map((_, i) => Promise.resolve(i)); // Créer 100 promesses
processInChunks(myPromises, 10) // Traiter 10 promesses Ă la fois
.then(results => console.log('Toutes les promesses sont résolues :', results));
Gérer les Erreurs avec Élégance
Une gestion correcte des erreurs est cruciale lorsque l'on travaille avec des Promesses. Utilisez des blocs try...catch pour attraper les erreurs qui pourraient survenir lors des opérations asynchrones. Envisagez d'utiliser des bibliothèques comme p-retry ou retry pour relancer automatiquement les opérations échouées.
async function fetchDataWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Erreur HTTP ! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
console.log(`Nouvelle tentative dans 1 seconde... (Tentatives restantes : ${retries})`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Attendre 1 seconde
return fetchDataWithRetry(url, retries - 1);
} else {
console.error('Nombre maximum de tentatives atteint. Opération échouée.');
throw error;
}
}
}
Utiliser Async/Await
async et await offrent une manière de travailler avec les Promesses qui ressemble davantage à du code synchrone. Ils peuvent améliorer considérablement la lisibilité et la maintenabilité du code.
N'oubliez pas d'utiliser des blocs try...catch autour des expressions await pour gérer les erreurs potentielles.
Annulation
Dans certains scénarios, vous pourriez avoir besoin d'annuler des Promesses en attente, en particulier lorsqu'il s'agit d'opérations de longue durée ou d'actions initiées par l'utilisateur. Vous pouvez utiliser l'API AbortController pour signaler qu'une Promesse doit être annulée.
const controller = new AbortController();
const signal = controller.signal;
async function fetchDataWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`Erreur HTTP ! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch annulé');
} else {
console.error('Erreur lors de la récupération des données :', error);
}
throw error;
}
}
fetchDataWithCancellation('https://api.example.com/data')
.then(data => console.log('Données reçues :', data))
.catch(error => console.error('Échec du fetch :', error));
// Annuler l'opération de fetch après 5 secondes
setTimeout(() => {
controller.abort();
}, 5000);
Conclusion
Les combinateurs de Promesses JavaScript sont des outils puissants pour construire des applications asynchrones robustes et efficaces. En comprenant les nuances de Promise.all, Promise.allSettled, Promise.race, et Promise.any, vous pouvez orchestrer des flux de travail asynchrones complexes, gérer les erreurs avec élégance et optimiser les performances. Lors du développement d'applications globales, il est crucial de prendre en compte la latence du réseau, les limites de débit des API et la fiabilité des sources de données. En appliquant les patrons et les bonnes pratiques discutés dans cet article, vous pouvez créer des applications JavaScript à la fois performantes et résilientes, offrant une expérience utilisateur supérieure aux utilisateurs du monde entier.